<polymer-element name="the-graph" attributes="graph menus library width height autolayout theme selectedNodes errorNodes selectedEdges animatedEdges getMenuDef pan scale maxZoom minZoom displaySelectionGroup forceSelection offsetY offsetX" touch-action="none">


  <template>
    <link rel="stylesheet" href="../themes/the-graph-dark.css">
    <link rel="stylesheet" href="../themes/the-graph-light.css">
    <div id="svgcontainer"></div>
  </template>

  <script type="text/javascript" src="./the-graph.js"></script>
  <script type="text/javascript" src="./font-awesome-unicode-map.js"></script>
  <script type="text/javascript" src="./the-graph-app.js"></script>
  <script type="text/javascript" src="./the-graph-graph.js"></script>
  <script type="text/javascript" src="./the-graph-node.js"></script>
  <script type="text/javascript" src="./the-graph-node-menu.js"></script>
  <script type="text/javascript" src="./the-graph-node-menu-port.js"></script>
  <script type="text/javascript" src="./the-graph-node-menu-ports.js"></script>
  <script type="text/javascript" src="./the-graph-port.js"></script>
  <script type="text/javascript" src="./the-graph-edge.js"></script>
  <script type="text/javascript" src="./the-graph-iip.js"></script>
  <script type="text/javascript" src="./the-graph-group.js"></script>
  <script type="text/javascript" src="./the-graph-tooltip.js"></script>
  <script type="text/javascript" src="./the-graph-menu.js"></script>
  <script type="text/javascript" src="./the-graph-clipboard.js"></script>

  <script>
  (function(){
    "use strict";

    Polymer('the-graph', {
      graph: null,
      library: null,
      menus: null,
      width: 800,
      height: 600,
      scale: 1,
      minZoom: 0.15,
      maxZoom: 15,
      appView: null,
      graphView: null,
      editable: true,
      autolayout: false,
      grid: 72,
      snap: 36,
      theme: "dark",
      selectedNodes: [],
      selectedNodesHash: {},
      errorNodes: {},
      selectedEdges: [],
      animatedEdges: [],
      autolayouter: null,
      displaySelectionGroup: true,
      forceSelection: false,
      offsetY: null,
      offsetX: null,
      created: function () {
        this.library = {};
        // Default pan
        this.pan = [0,0];
        // Initializes the autolayouter
        this.autolayouter = klayNoflo.init({
          onSuccess: this.applyAutolayout.bind(this),
          workerScript: "../bower_components/klayjs/klay.js"
        });
      },
      ready: function () {
        this.themeChanged();
      },
      themeChanged: function () {
        this.$.svgcontainer.className = "the-graph-"+this.theme;
      },
      graphChanged: function (oldGraph, newGraph) {
        if (oldGraph && oldGraph.removeListener) {
          oldGraph.removeListener("endTransaction", this.fireChanged);
        }
        // Listen for graph changes
        this.graph.on("endTransaction", this.fireChanged.bind(this));

        // Listen for autolayout changes
        if (this.autolayout) {
          this.graph.on('addNode', this.triggerAutolayout.bind(this));
          this.graph.on('removeNode', this.triggerAutolayout.bind(this));
          this.graph.on('addInport', this.triggerAutolayout.bind(this));
          this.graph.on('removeInport', this.triggerAutolayout.bind(this));
          this.graph.on('addOutport', this.triggerAutolayout.bind(this));
          this.graph.on('removeOutport', this.triggerAutolayout.bind(this));
          this.graph.on('addEdge', this.triggerAutolayout.bind(this));
          this.graph.on('removeEdge', this.triggerAutolayout.bind(this));
        }

        if (this.appView) {
          // Remove previous instance
          React.unmountComponentAtNode(this.$.svgcontainer);
        }

        // Setup app
        this.$.svgcontainer.innerHTML = "";
        this.appView = ReactDOM.render(
          window.TheGraph.App({
            graph: this.graph,
            width: this.width,
            minZoom: this.minZoom,
            maxZoom: this.maxZoom,
            height: this.height,
            library: this.library,
            menus: this.menus,
            editable: this.editable,
            onEdgeSelection: this.onEdgeSelection.bind(this),
            onNodeSelection: this.onNodeSelection.bind(this),
            onPanScale: this.onPanScale.bind(this),
            getMenuDef: this.getMenuDef,
            displaySelectionGroup: this.displaySelectionGroup,
            forceSelection: this.forceSelection,
            offsetY: this.offsetY,
            offsetX: this.offsetX
          }),
          this.$.svgcontainer
        );
        this.graphView = this.appView.refs.graph;
      },
      onPanScale: function (x, y, scale) {
        this.pan[0] = x;
        this.pan[1] = y;
        this.scale = scale;
      },
      onEdgeSelection: function (itemKey, item, toggle) {
        if (itemKey === undefined) {
          if (this.selectedEdges.length>0) {
            this.selectedEdges = [];
          }
          this.fire('edges', this.selectedEdges);
          return;
        }
        if (toggle) {
          var index = this.selectedEdges.indexOf(item);
          var isSelected = (index !== -1);
          var shallowClone = this.selectedEdges.slice();
          if (isSelected) {
            shallowClone.splice(index, 1);
            this.selectedEdges = shallowClone;
          } else {
            shallowClone.push(item);
            this.selectedEdges = shallowClone;
          }
        } else {
          this.selectedEdges = [item];
        }
        this.fire('edges', this.selectedEdges);
      },
      onNodeSelection: function (itemKey, item, toggle) {
        if (itemKey === undefined) {
          this.selectedNodes = [];
        } else if (toggle) {
          var index = this.selectedNodes.indexOf(item);
          var isSelected = (index !== -1);
          if (isSelected) {
            this.selectedNodes.splice(index, 1);
          } else {
            this.selectedNodes.push(item);
          }
        } else {
          this.selectedNodes = [item];
        }
        this.fire('nodes', this.selectedNodes);
        /*
          add by ajc
          额外发布一个选中的事件，因为选中node的时候还会发布 selectedNodesChanged
          避免使用 nodes 事件时，会额外调用一次事件handle
        */
        if(this.selectedNodes && this.selectedNodes.length){
          this.fire('nodes-select', this.selectedNodes);
        }
      },
      selectedNodesChanged: function () {
        var selectedNodesHash = {};
        for (var i=0, len = this.selectedNodes.length; i<len; i++) {
          selectedNodesHash[this.selectedNodes[i].id] = true;
        }
        this.selectedNodesHash = selectedNodesHash;
        this.fire('nodes', this.selectedNodes);
      },
      selectedNodesHashChanged: function () {
        if (!this.graphView) { return; }
        this.graphView.setSelectedNodes(this.selectedNodesHash);
      },
      errorNodesChanged: function () {
        if (!this.graphView) { return; }
        this.graphView.setErrorNodes(this.errorNodes);
      },
      selectedEdgesChanged: function () {
        if (!this.graphView) { return; }
        this.graphView.setSelectedEdges(this.selectedEdges);
        this.fire('edges', this.selectedEdges);
      },
      animatedEdgesChanged: function () {
        if (!this.graphView) { return; }
        this.graphView.setAnimatedEdges(this.animatedEdges);
      },
      fireChanged: function (event) {
        this.fire("changed", this);
      },
      autolayoutChanged: function () {
        if (!this.graph) { return; }
        // Only listen to changes that affect layout
        if (this.autolayout) {
          this.graph.on('addNode', this.triggerAutolayout.bind(this));
          this.graph.on('removeNode', this.triggerAutolayout.bind(this));
          this.graph.on('addInport', this.triggerAutolayout.bind(this));
          this.graph.on('removeInport', this.triggerAutolayout.bind(this));
          this.graph.on('addOutport', this.triggerAutolayout.bind(this));
          this.graph.on('removeOutport', this.triggerAutolayout.bind(this));
          this.graph.on('addEdge', this.triggerAutolayout.bind(this));
          this.graph.on('removeEdge', this.triggerAutolayout.bind(this));
        } else {
          this.graph.removeListener('addNode', this.triggerAutolayout);
          this.graph.removeListener('removeNode', this.triggerAutolayout);
          this.graph.removeListener('addInport', this.triggerAutolayout);
          this.graph.removeListener('removeInport', this.triggerAutolayout);
          this.graph.removeListener('addOutport', this.triggerAutolayout);
          this.graph.removeListener('removeOutport', this.triggerAutolayout);
          this.graph.removeListener('addEdge', this.triggerAutolayout);
          this.graph.removeListener('removeEdge', this.triggerAutolayout);
        }
      },
      triggerAutolayout: function (event) {
        var graph = this.graph;
        var portInfo = this.graphView ? this.graphView.portInfo : null;
        // Calls the autolayouter
        this.autolayouter.layout({
          "graph": graph,
          "portInfo": portInfo,
          "direction": "RIGHT",
          "options": {
            "intCoordinates": true,
            "algorithm": "de.cau.cs.kieler.klay.layered",
            "layoutHierarchy": true,
            "spacing": 36,
            "borderSpacing": 20,
            "edgeSpacingFactor": 0.2,
            "inLayerSpacingFactor": 2.0,
            "nodePlace": "BRANDES_KOEPF",
            "nodeLayering": "NETWORK_SIMPLEX",
            "edgeRouting": "POLYLINE",
            "crossMin": "LAYER_SWEEP",
            "direction": "RIGHT"
          }
        });
      },
      applyAutolayout: function (layoutedKGraph) {
        this.graph.startTransaction("autolayout");

        // Update original graph nodes with the new coordinates from KIELER graph
        var children = layoutedKGraph.children.slice();

        var i, len;
        for (i=0, len = children.length; i<len; i++) {
          var klayNode = children[i];
          var nofloNode = this.graph.getNode(klayNode.id);

          // Encode nodes inside groups
          if (klayNode.children) {
            var klayChildren = klayNode.children;
            var idx;
            for (idx in klayChildren) {
              var klayChild = klayChildren[idx];
              if (klayChild.id) {
                this.graph.setNodeMetadata(klayChild.id, {
                  x: Math.round((klayNode.x + klayChild.x)/this.snap)*this.snap,
                  y: Math.round((klayNode.y + klayChild.y)/this.snap)*this.snap
                });
              }
            }
          }

          // Encode nodes outside groups
          if (nofloNode) {
            this.graph.setNodeMetadata(klayNode.id, {
              x: Math.round(klayNode.x/this.snap)*this.snap,
              y: Math.round(klayNode.y/this.snap)*this.snap
            });
          } else {
            // Find inport or outport
            var idSplit = klayNode.id.split(":::");
            var expDirection = idSplit[0];
            var expKey = idSplit[1];
            if (expDirection==="inport" && this.graph.inports[expKey]) {
              this.graph.setInportMetadata(expKey, {
                x: Math.round(klayNode.x/this.snap)*this.snap,
                y: Math.round(klayNode.y/this.snap)*this.snap
              });
            } else if (expDirection==="outport" && this.graph.outports[expKey]) {
              this.graph.setOutportMetadata(expKey, {
                x: Math.round(klayNode.x/this.snap)*this.snap,
                y: Math.round(klayNode.y/this.snap)*this.snap
              });
            }
          }
        }

        this.graph.endTransaction("autolayout");

        // Fit to window
        this.triggerFit();

      },
      triggerFit: function () {
        if (this.appView) {
          this.appView.triggerFit();
        }
      },
      widthChanged: function () {
        if (!this.appView) { return; }
        this.appView.setState({
          width: this.width
        });
      },
      heightChanged: function () {
        if (!this.appView) { return; }
        this.appView.setState({
          height: this.height
        });
      },
      updateIcon: function (nodeId, icon) {
        if (!this.graphView) { return; }
        this.graphView.updateIcon(nodeId, icon);
      },
      rerender: function (options) {
        // This is throttled with rAF internally
        if (!this.graphView) { return; }
        this.graphView.markDirty(options);
      },
      addNode: function (id, component, metadata) {
        if (!this.graph) { return; }
        this.graph.addNode(id, component, metadata);
      },
      getPan: function () {
        if (!this.appView) {
          return [0, 0];
        }
        return [this.appView.state.x, this.appView.state.y];
      },
      panChanged: function () {
        // Send pan back to React
        if (!this.appView) { return; }
        this.appView.setState({
          x: this.pan[0],
          y: this.pan[1]
        });
      },
      getScale: function () {
        if (!this.appView) {
          return 1;
        }
        return this.appView.state.scale;
      },
      displaySelectionGroupChanged: function () {
        if (!this.graphView) { return; }
        this.graphView.setState({
          displaySelectionGroup: this.displaySelectionGroup
        });
      },
      forceSelectionChanged: function () {
        if (!this.graphView) { return; }
        this.graphView.setState({
          forceSelection: this.forceSelection
        });
      },
      focusNode: function (node) {
        this.appView.focusNode(node);
      },
      menusChanged: function () {
        // Only if the object itself changes,
        // otherwise builds menu from reference every time menu shown
        if (!this.appView) { return; }
        this.appView.setProps({menus: this.menus});
      },
      debounceLibraryRefeshTimer: null,
      debounceLibraryRefesh: function () {
        // Breaking the "no debounce" rule, this fixes #76 for subgraphs
        if (this.debounceLibraryRefeshTimer) {
          clearTimeout(this.debounceLibraryRefeshTimer);
        }
        this.debounceLibraryRefeshTimer = setTimeout(function () {
          this.rerender({libraryDirty:true});
        }.bind(this), 200);
      },
      mergeComponentDefinition: function (component, definition) {
        // In cases where a component / subgraph ports change,
        // we don't want the connections hanging in middle of node
        // TODO visually indicate that port is a ghost
        if (component === definition) {
          return definition;
        }
        var _i, _j, _len, _len1, exists;
        var cInports = component.inports;
        var dInports = definition.inports;

        if (cInports !== dInports) {
          for (_i = 0, _len = cInports.length; _i < _len; _i++) {
            var cInport = cInports[_i];
            exists = false;
            for (_j = 0, _len1 = dInports.length; _j < _len1; _j++) {
              var dInport = dInports[_j];
              if (cInport.name === dInport.name) {
                exists = true;
              }
            }
            if (!exists) {
              dInports.push(cInport);
            }
          }
        }

        var cOutports = component.outports;
        var dOutports = definition.outports;

        if (cOutports !== dOutports) {
          for (_i = 0, _len = cOutports.length; _i < _len; _i++) {
            var cOutport = cOutports[_i];
            exists = false;
            for (_j = 0, _len1 = dOutports.length; _j < _len1; _j++) {
              var dOutport = dOutports[_j];
              if (cOutport.name === dOutport.name) {
                exists = true;
              }
            }
            if (!exists) {
              dOutports.push(cOutport);
            }
          }
        }

        if (definition.icon !== 'cog') {
          // Use the latest icon given
          component.icon = definition.icon;
        } else {
          // we should use the icon from the library
          definition.icon = component.icon;
        }
        // a component could also define a svg icon
        definition.iconsvg = component.iconsvg;

        return definition;
      },
      registerComponent: function (definition, generated) {
        var component = this.library[definition.name];
        var def = definition;
        if (component) {
          if (generated) {
            // Don't override real one with generated dummy
            return;
          }
          def = this.mergeComponentDefinition(component, definition);
        }
        this.library[definition.name] = def;
        // So changes are rendered
        this.debounceLibraryRefesh();
      },
      getComponent: function (name) {
        return this.library[name];
      },
      toJSON: function () {
        if (!this.graph) { return {}; }
        return this.graph.toJSON();
      }
    });

  })();
  </script>
</polymer-element>
